Een Controller klasse maken
Knoppen staan op formulieren, al een geluk. Maar de code, die de interactie met de gebruiker afhandelt, staat heel dikwijls overal versnipperd. Heel wat redirects waarmee je van her naar der springt. Redirects raad ik af want moeilijk te debuggen. De afhandeling van de interactie staat waarlijk overal en nergens. Hoe kan je in godsnaam de workflow volgen? Is het geen idee om alle interactie met de gebruiker op één plaats te centraliseren in wat in het MVC patroon, de controller wordt genoemd?
- We maken één enkele routing switch die alle interactie met de gebruiker afhandelt.
- We gebruiken reflectie om de klasse te instantiëren en de methode uit te voeren
Masterpage
De website heeft slechts één pagina, namelijk index.php, die in de root van de website staat. Dat maakt het navigeren overzichtelijker en het onderhouden van de verschillende views binnen de website gemakkelijker.
Use cases
Een volgende stap is het opstellen van use cases. Je gaat zien dat met elke use case een knop overeenstemt. Ik heb het niet over systeem use cases maar over gebruikers use cases. We hebben een manier nodig om use cases een naam te geven. Daarvoor laten we ons inspireren door REST. Een use case bestaat uit
- de namespace waarin de entiteit zich bevindt;
- de naam van de entiteit (object/klasse);
- de naam van de actiemethode;
- een id parameter;
De drie elementen worden gescheiden door een schuine streep /.
Voorbeelden:
- Customer/create
- ModernWays/Customer/create
- ModernWays/FricFrac/Customer/create
- Custumer/update/14
- Customer/delete/5
- Customer/readOne/6
- Customer/readAll
De naam van de entiteit is de naam van de controller en de naam van de actie is de naam van de methode van de controller die moet worden uitgevoerd.
Routing regels
- 1 element: de naam verwijst naar een controller klasse in de standaard namespace;
- 2 elementen: de eerste naam verwijst naar de controller klasse in de standaard namespace en de tweede naar de methode;
- 3 of meer elementen:
- is het laatste element numeriek dan verwijst alles wat voor het derdelaatste element komt naar de namespace, het derdelaatste element naar de controller klasse in de standaard namespace, de tweedelaatste naar de methode en de laatste is de parameter waarde; is er geen namespace opgegeven wordt de standaard namespace gebruikt;
- is het laatste element niet numeriek dan is het derdelaatste element en alles wat er voor komt de namespace, het tweede de controller en het derde de methode;
Code voor dispatcher
We schrijven een methode in het bestand index.php, die de URL analyseert en in betekenisvolle onderdelen opbreekt:
<?php function dispatchPlus($route, $defaultNamespace = '/') { // remove first /, if present $route = ltrim($route, '/'); $routeParts = explode('/', $route); $namespaceName = $defaultNamespace; $countRouteParts = count($routeParts); if ($countRouteParts === 1) { // klassenamen in PHP beginnen met een hoofdletter, pascalnotatie $controllerName = ucfirst($routeParts[0]); // functies in camelcasenotatie $actionMethodName = 'index'; $parameterValue = -1; } elseif ($countRouteParts === 2) { $controllerName = ucfirst($routeParts[0]); $actionMethodName = lcfirst($routeParts[1]); $parameterValue = -1; } else { if (is_numeric(end($routeParts))) { $controllerName = ucfirst($routeParts[$countRouteParts - 3]); $actionMethodName = lcfirst($routeParts[$countRouteParts - 2]); $parameterValue = $routeParts[$countRouteParts - 1]; } else { $namespaceName = ucfirst($routeParts[$countRouteParts - 3]); $controllerName = ucfirst($routeParts[$countRouteParts - 2]); $actionMethodName = lcfirst($routeParts[$countRouteParts - 1]); $parameterValue = -1; } if (count($routeParts) > 3) { $path = implode('/', array_map('ucfirst', array_slice($routeParts, 0, $countRouteParts - 3))); $namespaceName = $path . '/' . $namespaceName; } } echo '<p>Namespacenaam: ' . $namespaceName . '</p>'; echo '<p>Controllernaam: ' . $controllerName . '</p>'; echo '<p>Actiemethodenaam: ' . $actionMethodName . '</p>'; echo '<p>Parameter waarde: ' . $parameterValue . '</p>'; } if (isset($_SERVER['REDIRECT_URL'])) { dispatchPlus($_SERVER['REDIRECT_URL']); }
Om de code te testen moet die staan in index.php. Deze code staat in mijn workspace op Cloud9 in het bestand met de naam index-prepare-controller.php.
Let op het gebruik van:
implode
retourneert een string die bestaat alle elementen van de opgegeven array gescheiden door de opgegeven scheidingstekenreeks.array_slice
om een deel van een opgegeven array, te kopiëren en te retourneren, te beginnen van een opgegeven startelement tot een opgegeven eindelement;array_map
doorloopt alle elementen van de array en voort de functie die als eerste argument wordt meegegeven op elk element uit en retourneert het resultaat;
Refactoring
- Ontwerp
Het wordt tijd om de procedurale werkwijze te verlaten en OO te beginnen werken. We gaan een klasse maken met de naam
Controller
. Deze klasse zetten we in een namespace die naam van de vendor en van het product bevat. In dit geval isModernWays
. ModernWays is de naam van het bedrijf dat het product maakt.We kiezen ervoor om een static klasse met static members te maken. Dan moeten we geen instantie van de klasse maken vooraleer die te gebruiken en we beschikken tegelijkertijd over een singleton klasse.
Controller klasse Members Naam Scope Beschrijnving velden $namespaceName public $controllerName public $actionMethodName public $parameterValue public methoden dispatch public breekt de url op in een controllernaam, actiemethode en parameter waarde dispatchPlus public breekt de url op in een namespacenaam, controllernaam, actiemethode en parameter waarde - Mappenstructuur
We bereiden ons voor op het gebruik van Composer, een packagemanager voor PHP, en plaatsen onze klassen in een mappenstructuur zoals vereist:|vendor |modernways |src |Controller.php
- De dispatcher toevoegen
Je gebruikt
DispatchPlus
als je een namespace op de url wil meegeven.<?php /** * Created by ModernWays * User: Jef Inghelbrecht * Date: 10/04/2019 * Time: 10:32 */ namespace ModernWays; // tussen de declaratie van de namespace en de rest 1 lege regel. class Controller { static public $namespaceName; static public $controllerName; static public $actionMethodName; static public $parameterValue; /** * breekt de url op in een controllernaam, actiemethode en * parameter waarde * @param null $route * @return string */ public static function dispatch($route, $defaultNamespace = '/') { self::$namespaceName = $defaultNamespace; self::$controllerName = 'Home'; self::$actionMethodName = 'index'; self::$parameterValue = -1;// Standard parameter. Shouldn't appear in the database. // remove first /, if present $route = ltrim($route, '/'); $routeParts = explode('/', $route); if (isset($routeParts[0])) { // klassenamen in PHP beginnen met een hoofdletter, pascalnotatie self::$controllerName = ucfirst($routeParts[0]); } if (isset($routeParts[1])) { // functies in camelcasenotatie self::$actionMethodName = lcfirst($routeParts[1]); } if (isset($routeParts[2])) { self::$parameterValue = $routeParts[2]; } return '<p>Controllernaam: ' . self::$controllerName . '</p>' . '<p>Actiemethodenaam: ' . self::$actionMethodName . '</p>' . '<p>Parameter waarde: ' . self::$parameterValue . '</p>'; } /** * breekt de url op in een namespacenaam, controllernaam, * actiemethode en parameter waarde * @param route * @param $defaultNamespace * @return string */ function dispatchPlus($route, $defaultNamespace = '/') { // remove first /, if present $route = ltrim($route, '/'); $routeParts = explode('/', $route); self::$namespaceName = $defaultNamespace; $countRouteParts = count($routeParts); if ($countRouteParts === 1) { // klassenamen in PHP beginnen met een hoofdletter, pascalnotatie self::$controllerName = ucfirst($routeParts[0]); // functies in camelcasenotatie self::$actionMethodName = 'index'; self::$parameterValue = -1; } elseif ($countRouteParts === 2) { self::$controllerName = ucfirst($routeParts[0]); self::$actionMethodName = lcfirst($routeParts[1]); self::$parameterValue = -1; } else { if (is_numeric(end($routeParts))) { self::$controllerName = ucfirst($routeParts[$countRouteParts - 3]); self::$actionMethodName = lcfirst($routeParts[$countRouteParts - 2]); self::$parameterValue = $routeParts[$countRouteParts - 1]; } else { self::$namespaceName = ucfirst($routeParts[$countRouteParts - 3]); self::$controllerName = ucfirst($routeParts[$countRouteParts - 2]); self::$actionMethodName = lcfirst($routeParts[$countRouteParts - 1]); self::$parameterValue = -1; } if (count($routeParts) > 3) { $path = implode('/', array_map('ucfirst', array_slice($routeParts, 0, $countRouteParts - 3))); self::$namespaceName = $path . '/' . self::$namespaceName; } } return '<p>Namespacenaam: ' . self::$namespaceName . '</p>' . '<p>Controllernaam: ' . self::$controllerName . '</p>' . '<p>Actiemethodenaam: ' . self::$actionMethodName . '</p>' . '<p>Parameter waarde: ' . self::$parameterValue . '</p>'; } }
- De klasse testen
Kopiëer wat er in
index.php
staat naarindex-prepare-controller.php
. We gaan deindex.php
pagina nu gebruiken om deController
klasse uit te proberen.
We beginnen met het Controller.php bestand in te sluiten met deinclude
instructie:<?php include ('vendor/modernways/src/Controller.php'); $redirectUrl = 'Home/index'; $namespaceName = 'ModernWays'; if (isset($_SERVER['REDIRECT_URL'])) { $redirectUrl = $_SERVER['REDIRECT_URL']; } echo '<h1>Resultaat Dispatch:</h1>'; echo ModernWays\Controller::dispatch($redirectUrl); echo '<h1>Resultaat DispatchPlus:</h1>'; echo ModernWays\Controller::dispatchPlus($redirectUrl, $namespaceName);
- Resultaat
- De actiemethode uitvoeren
De dispatcher heeft de naam van de controller en van de actiemethode uit de url gefilterd. We gaan reflection gebruiken om een de methode van de controllerklasse uit te voeren.
Voeg de volgende methode aan de
Controller
klasse toe:/** * Creates an instance of itself with the name passed in $entity * and invokes the method with the name passed in $action * @return bool|mixed */ public static function invokeAction() { // Functions, method calls, static class variables, and class constants inside {$} work since PHP 5. // However, the value accessed will be interpreted as the name of a variable in the scope in which the // string is defined. Using single curly braces ({}) will not work for accessing the return values of // functions or methods or the values of class constants or static class variables. $namespaceName = self::$namespaceName; $controllerName = self::$controllerName; $actionMethodName = self::$actionMethodName; $id = self::$parameterValue; $controllerName = "\\{$namespaceName}\\Controllers\\{$controllerName}Controller"; $actionMethod = new \ReflectionMethod($controllerName, $actionMethodName); if (!class_exists($controllerName, true)) { return false; } else { $reflection = new \ReflectionClass($controllerName); $controller = $reflection->newInstance(); return $actionMethod->invokeArgs($controller, array($id)); } }
- Een Controller klasse maken voor Student
We gaan de
Controller
klasse nu toepassen. We maken eenStudentController
klasse die overerft van deController
klasse. In deze klasse maken we een methode met de naamreadingAll die alle studenten oplijst. Volgens de afspraak - we volgen de afspraken van ASP.NET MVC - staan de controllers in een map met de naam Controllers.
Het model is hier een gewone array. Later zien we hoe de controller het model opvult vanuit een database.
Plaats de code in een bestand met de naam /Controllers/StudentController.php:<?php /** * Created by ModernWays * User: Jef Inghelbrecht * Date: 10/04/2019 * Time: 10:32 */ namespace ModernWays\Controllers; class StudentController extends \ModernWays\Controller { // het model vullen we later met een DAL private $student = array('Jef Inghelbrecht', 'Kees Baaten', 'Liesbeth Baaten', 'Mohammed El Farisi'); public function readingAll() { echo '<h1>Studenten</h1>' . '<table>' . '<caption>Studenten</caption>' . '<tr>' . '<th>Naam</th>' . '</tr>'; for ($i = 0; $i < count($model); $i++) { echo '<tr><td>' . $model[$i] . '</td></tr>'; } } }
- De StudentController klasse testen
- We beginnen met de Controller.php en StudentController.php bestanden in te sluiten met de
include
instructie.
In de index.php pagina typ je het volgende:<?php include ('vendor/modernways/src/Controller.php'); include ('Controllers/StudentController.php'); if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { setlocale(LC_ALL, 'nld_nld'); } else { setlocale(LC_ALL, 'nlb'); } $redirectUrl = 'Home/index'; if (isset($_SERVER['REDIRECT_URL'])) { $redirectUrl = $_SERVER['REDIRECT_URL']; } ModernWays\Controller::dispatchPlus($redirectUrl, 'ModernWays'); ModernWays\Controller::invokeAction();
- in de url typ je:
https://programmeren4-16570-jef-jefinghelbrecht.c9users.io/Student/ReadingAll
- Met dit als resultaat:
- We beginnen met de Controller.php en StudentController.php bestanden in te sluiten met de
- De View implementeren
De controller is de koppeling tussen de gebruikersinterface (view) en de verwerkingslogica (model). De controller gebruikt de modelmethoden voor het ophalen van informatie over het applicatie-object (ding, entiteit) of voor het wijzigen van de status van het object. De controller informeert vervolgens de view over de wijzigingen.
We gaan nu leren hoe de controller de view kan informeren over de wijzigingen.
- De view methode
In de
Controller
klasse voegen we en methode toe die de view retourneert die aan de gebruiker moet worden getoond.We moeten het model, dat in de controller wordt bijgewerkt, en het pad waar de view zich bevindt aan de View meegeven. We moeten dus de data bewaren tot het moment dat de view functie wordt uitgevoerd. Dat doen we met behulp van een PHP closure.
public static function view($model = null, $path = null) { // als het pad naar de view niet werd opgegeven en dus null is, // staat het in een submap met de naam Views en heeft het // if (!isset($path)) { $trace = debug_backtrace(); // echo '<pre>' . var_dump($trace) . '</pre>'; $method = ucfirst($trace[1]["function"]); $class = $trace[1]["class"]; $class = substr($class, strrpos($class, '\\') + 1); $class = str_replace('Controller', '', $class); $path = "Views/{$class}/{$method}.php"; } $view = function () use ($model, $path) { include($path); }; return $view; }
- De view zelf
De view zelf is een php bestand dat volgens de afspraken binnen ASP.NET MVC in een map met de naam Views staat. In deze map moet een submap met dezelfde naam als de controller staan. In dit geval hier is dat Student. En in de Studentmap een php bestand met dezelfde naam als de actiemethode van de controller. In ons voorbeeld is datReadingAllWithView
: - De code in ReadingAllWithView.php:
<h1>Studenten</h1> <table> <caption>Studenten</caption> <tr> <th>Naam</th> </tr> <?php for ($i = 0; $i < count($model); $i++) { ?> <tr> <td><?php echo $model[$i];?></td> </tr> <?php } ?> </table>
- De
view
methode gebruikenAls de gebruiker de volgende url intypt
https://programmeren4-16570-jef-jefinghelbrecht.c9users.io/Student/ReadingAllWithView
dan moet de controller met de naam
StudentController
geïnstancieerd worden en de methode met de naamreadingAllWithView
uitgevoerd worden. Standaard retourneert deze methode een view met de naam Views/Student/ReadingAllWithView.php:public function readingAllWithView() { return $this->view($this->student); }
Wil je een view met een andere naam weergeven moet je het volledige pad opgeven. Neem aan dat je view als naam ReadingAllWithTileView.php heeft dan moet je het volgende pad meegeven:
public function readingAllWithView() { return $this->view($this->student, 'Views/Student/ReadingAllWithTileView.php'); }
- De StudentController klasse testen met view
De index.php pagina passen we aan om de view methode te kunnen gebruiken. Let erop dat we aan de
view
methode gaan pad naar de view meegeven omdat die in de standaard map staat, namelijk Views/Controllernaam/ActionMethod.php:<?php include ('vendor/modernways/src/Controller.php'); include ('Controllers/StudentController.php'); $redirectUrl = 'Home/index'; if (isset($_SERVER['REDIRECT_URL'])) { $redirectUrl = $_SERVER['REDIRECT_URL']; } ModernWays\Controller::dispatchPlus($redirectUrl, 'ModernWays'); $view = ModernWays\Controller::invokeAction(); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>PHP MVC</title> </head> <body> <?php $view(); ?> </body> </html>
Met de methode
dispatchPlus
breken we de url op in betekenisvolle onderdelen. De methodeinvokeAction
maakt een instantie van de controllerklasse en voert de methode uit die in de url werden opgegeven. DeinvokeAction
methode retourneert de view die in het browservenster zal worden getoond:<body> <?php $view(); ?> </body>
- De view methode
De volledige code
- vendor/modernways/src/Controller.php
<?php /** * Created by ModernWays * User: Jef Inghelbrecht * Date: 10/04/2019 * Time: 10:32 */ namespace ModernWays; // tussen de declaratie van de namespace en de rest 1 lege regel. class Controller { static public $namespaceName; static public $controllerName; static public $actionMethodName; static public $parameterValue; /** * breekt de url op in een controllernaam, actiemethode en * parameter waarde * @param null $route * @return string */ public static function dispatch($route, $defaultNamespace = '/') { self::$namespaceName = $defaultNamespace; self::$controllerName = 'Home'; self::$actionMethodName = 'index'; self::$parameterValue = -1;// Standard parameter. Shouldn't appear in the database. // remove first /, if present $route = ltrim($route, '/'); $routeParts = explode('/', $route); if (isset($routeParts[0])) { // klassenamen in PHP beginnen met een hoofdletter, pascalnotatie self::$controllerName = ucfirst($routeParts[0]); } if (isset($routeParts[1])) { // functies in camelcasenotatie self::$actionMethodName = lcfirst($routeParts[1]); } if (isset($routeParts[2])) { self::$parameterValue = $routeParts[2]; } return '<p>Controllernaam: ' . self::$controllerName . '</p>' . '<p>Actiemethodenaam: ' . self::$actionMethodName . '</p>' . '<p>Parameter waarde: ' . self::$parameterValue . '</p>'; } /** * breekt de url op in een namespacenaam, controllernaam, * actiemethode en parameter waarde * @param route * @param $defaultNamespace * @return string */ function dispatchPlus($route, $defaultNamespace = '/') { // remove first /, if present $route = ltrim($route, '/'); $routeParts = explode('/', $route); self::$namespaceName = $defaultNamespace; $countRouteParts = count($routeParts); if ($countRouteParts === 1) { // klassenamen in PHP beginnen met een hoofdletter, pascalnotatie self::$controllerName = ucfirst($routeParts[0]); // functies in camelcasenotatie self::$actionMethodName = 'index'; self::$parameterValue = -1; } elseif ($countRouteParts === 2) { self::$controllerName = ucfirst($routeParts[0]); self::$actionMethodName = lcfirst($routeParts[1]); self::$parameterValue = -1; } else { if (is_numeric(end($routeParts))) { self::$controllerName = ucfirst($routeParts[$countRouteParts - 3]); self::$actionMethodName = lcfirst($routeParts[$countRouteParts - 2]); self::$parameterValue = $routeParts[$countRouteParts - 1]; } else { self::$namespaceName = ucfirst($routeParts[$countRouteParts - 3]); self::$controllerName = ucfirst($routeParts[$countRouteParts - 2]); self::$actionMethodName = lcfirst($routeParts[$countRouteParts - 1]); self::$parameterValue = -1; } if (count($routeParts) > 3) { $path = implode('/', array_map('ucfirst', array_slice($routeParts, 0, $countRouteParts - 3))); self::$namespaceName = $path . '/' . self::$namespaceName; } } return '<p>Namespacenaam: ' . self::$namespaceName . '</p>' . '<p>Controllernaam: ' . self::$controllerName . '</p>' . '<p>Actiemethodenaam: ' . self::$actionMethodName . '</p>' . '<p>Parameter waarde: ' . self::$parameterValue . '</p>'; } /** * Creates an instance of itself with the name passed in $entity * and invokes the method with the name passed in $action * @return bool|mixed */ public static function invokeAction() { // Functions, method calls, static class variables, and class constants inside {$} work since PHP 5. // However, the value accessed will be interpreted as the name of a variable in the scope in which the // string is defined. Using single curly braces ({}) will not work for accessing the return values of // functions or methods or the values of class constants or static class variables. $namespaceName = self::$namespaceName; $controllerName = self::$controllerName; $actionMethodName = self::$actionMethodName; $id = self::$parameterValue; $controllerName = "\\{$namespaceName}\\Controllers\\{$controllerName}Controller"; $actionMethod = new \ReflectionMethod($controllerName, $actionMethodName); if (!class_exists($controllerName, true)) { return false; } else { $reflection = new \ReflectionClass($controllerName); $controller = $reflection->newInstance(); return $actionMethod->invokeArgs($controller, array($id)); } } public static function view($model = null, $path = null) { // als het pad naar de view niet werd opgegeven en dus null is, // staat het in een submap met de naam Views en heeft het // if (!isset($path)) { $trace = debug_backtrace(); // echo '<pre>' . var_dump($trace) . '</pre>'; $method = ucfirst($trace[1]["function"]); $class = $trace[1]["class"]; $class = substr($class, strrpos($class, '\\') + 1); $class = str_replace('Controller', '', $class); $path = "Views/{$class}/{$method}.php"; } $view = function () use ($model, $path) { include($path); }; return $view; } }
- Controllers/StudentController.php
<?php /** * Created by ModernWays * User: Jef Inghelbrecht * Date: 10/04/2019 * Time: 10:32 */ namespace ModernWays\Controllers; class StudentController extends \ModernWays\Controller { // het model vullen we later met een DAL private $student = array('Jef Inghelbrecht', 'Kees Baaten', 'Liesbeth Baaten', 'Mohammed El Farisi'); public function readingAll() { $model = $this->student; echo '<h1>Studenten</h1>' . '<table>' . '<caption>Studenten</caption>' . '<tr>' . '<th>Naam</th>' . '</tr>'; for ($i = 0; $i < count($model); $i++) { echo '<tr><td>' . $model[$i] . '</td></tr>'; } } public function readingAllWithView() { return $this->view($this->student); } }
- Views/Student/ReadingAllWithView.php
<h1>Studenten</h1> <table> <caption>Studenten</caption> <tr> <th>Naam</th> </tr> <?php for ($i = 0; $i < count($model); $i++) { ?> <tr> <td><?php echo $model[$i];?></td> </tr> <?php } ?> </table>
- index.php
<?php include ('vendor/modernways/src/Controller.php'); include ('Controllers/StudentController.php'); if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { setlocale(LC_ALL, 'nld_nld'); } else { setlocale(LC_ALL, 'nlb'); } $redirectUrl = 'Home/index'; if (isset($_SERVER['REDIRECT_URL'])) { $redirectUrl = $_SERVER['REDIRECT_URL']; } ModernWays\Controller::dispatchPlus($redirectUrl, 'ModernWays'); $view = ModernWays\Controller::invokeAction(); ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>PHP MVC</title> </head> <body> <?php $view(); ?> </body> </html>